'''
Created on 04.06.2013

@author: Michael
'''

from libavg import avg
import random

'''
This class delivers a set of graphics for drawing the playing field.
One can reset the default fieldGraphics object of the field node and the player stats by a custom one.
'''
class FieldGraphics(object):
    def __init__(self, destroyable="./images/block_destroyable.png", undestroyable="./images/block_undestroyable.png", 
                 back="./images/default_back.jpg", bomb="./images/bomb.png", bomb_field="./images/bomb_field.png", speed="./images/speed.png", strength="./images/strength.png",
                 upgrade_bomb="./images/block_upgrade_bomb.png", upgrade_speed="./images/block_upgrade_speed.png", upgrade_strength="./images/block_upgrade_strength.png"):
        self.destroyable = destroyable
        self.undestroyable = undestroyable
        self.back = back
        self.bomb = bomb
        self.bomb_field = bomb_field
        self.speed = speed
        self.strength = strength
        self.upgrade_bomb = upgrade_bomb
        self.upgrade_speed = upgrade_speed
        self.upgrade_strength = upgrade_strength

'''
BlockNode represents one rectangular unit one the field with fixed position.
It holds information on what is going on directly on its position.

IMPORTANT: can either show a block, a upgrade or a bomb!
Cannot show two or more images at the same time...
'''
class BlockNode(avg.ImageNode):
    # is there an upgrade?
    __upgrade = -1  # -1: no | 0: bomb | 1: speed | 2: strength
    
    # is there a player? only for collision check not for visualization
    __playerID = -1 # -1: no | i={0, 1, 2, 3}: player_i
    
    # what block type is it?
    __blockType = -1 # -1: default | 0: destroyable | 1: undestroyable
    
    # is a bomb here?
    __bomb = False
    
    # is there an explosion?
    __explosion = False
    
    __animObj = None
    
    def __init__(self, fieldParent, size, pos, fieldGraphics):
        self.__size = size
        self.__pos = pos
        self.__fieldGraphics = fieldGraphics
        self.__fieldParent = fieldParent
        self.__suddenDeath = False

        # init ImageNode
        super(BlockNode, self).__init__(size=self.__size, pos=(pos[0] * size[0], pos[1] * size[1]))
        self.registerInstance(self, self.__fieldParent)

    def getPos(self):
        return self.__pos

    def __removeImage(self):
        self.href = ""
        
    def __resetState(self):
        self.__bomb = False
        self.__blockType = -1
        self.__upgrade = -1
        
    def __showImage(self, image):
        self.href = image
        #self.setBitmap(avg.Bitmap(image))
        
    def isWalkable(self):
        return (self.__bomb == 0 and self.__blockType == -1)
    
    # -1: no block | 0: destroyable block | 1: undestroyable block           
    def setBlockType(self, blockType):
        self.__resetState()
        self.__blockType = blockType
        if blockType == -1:
            self.removeBlock()
        elif blockType == 0:
            self.__showImage(self.__fieldGraphics.destroyable)
        elif blockType == 1:
            self.__showImage(self.__fieldGraphics.undestroyable)
            
    def removeBlock(self):
        if self.__blockType != -1:
            self.__blockType = -1
            self.__removeImage()

    def getBlockType(self):
        return self.__blockType
    
    # -1: no upgrade | 0: bomb up | 1: speed up | 2: strength up
    def setUpgrade(self, upgrade):
        self.__resetState()
        self.__upgrade = upgrade
        if upgrade == -1:
            self.removeUpgrade()
        elif upgrade == 0:
            self.__showImage(self.__fieldGraphics.upgrade_bomb)
        elif upgrade == 1:
            self.__showImage(self.__fieldGraphics.upgrade_speed)
        elif upgrade == 2:
            self.__showImage(self.__fieldGraphics.upgrade_strength)
        
    def getUpgrade(self):
        return self.__upgrade
    
    def removeUpgrade(self):
        if self.__upgrade != -1:
            self.__removeImage()
            self.__upgrade = -1
    
    def addBomb(self, userNode, detonationCallback):
        # TODO: show countdown?
        if not self.isBombPlaced() and not userNode.isDead():
            self.__resetState()
            self.__bomb = True
            user = userNode.getUser()
            user.bombPlaced()
            self.__showImage(self.__fieldGraphics.bomb_field)
            self.__animateBomb(userNode, False, detonationCallback)
        
    def __animateBomb(self, userNode, visible, detonation):
        if not visible:
            self.__animObj = avg.LinearAnim(self, "opacity", 2750, 1, 0.4)
            self.__animObj.setStopCallback(lambda: self.__animateBomb(userNode, True, detonation))
            self.__animObj.start()
        else:
            self.__animObj = avg.LinearAnim(self, "opacity", 250, 0.4, 1)
            self.__animObj.setStopCallback(lambda: detonation(userNode, self))
            self.__animObj.start()
    
    # starts explosion        
    def explode(self):
        self.__explode(self.explosionEnds)
            
    def __explode(self, callback, time=500, suddenDeath=False):
        self.__explosion = True
        # detonate bomb if available
        if self.isBombPlaced() and not suddenDeath:
            self.__animObj.abort() # calls detonation callback
            return
        
        if self.isBombPlaced() and suddenDeath:
            self.__suddenDeath = True
            self.__animObj.abort() # calls detonation callback
            
        # show explosion
        if self.isExploding() and not self.__animObj == None:
            self.__animObj.abort()
        self.__showImage(self.__fieldGraphics.strength)
        self.__animObj = avg.LinearAnim(self, "opacity", time, 1, 0.5)
        self.__animObj.setStopCallback(callback)
        self.__animObj.start()
    
    # removes explosion
    def explosionEnds(self):
        self.__removeImage()
        self.__explosion = False
        if self.__suddenDeath:
            self.setBlockType(1)
            self.opacity = 1
    
    def isExploding(self):
        return self.__explosion
    
    def removeBomb(self):
        if self.__bomb:
            self.__bomb = False
            self.__removeImage()
    
    def isBombPlaced(self):
        return self.__bomb
    
    # starts sudden death explosion -> does not abort bombs
    def suddenDeath(self, callback):
        self.__explode(lambda: self.__suddenDeathEnd(callback), time=250, suddenDeath=True)
        
    def __suddenDeathEnd(self, callback):
        self.explosionEnds()
        self.setBlockType(1)
        self.opacity = 1
        callback()

'''
This class represents one user on the field.
This node can be moved over the field node.
Additionally this node shows a walking animation.
'''
class UserNode(avg.ImageNode):
    direction = None 
    running = False

    def __init__(self, fieldParent, size, user):
        self.__size = size
        self.__pos = (user.pos[0] * size[0], user.pos[1] * size[1])
        self.__user = user
        self.__fieldParent = fieldParent

        # init ImageNode
        super(UserNode, self).__init__(href=user.getCharImage(2) ,size=self.__size, pos=self.__pos)
        self.registerInstance(self, self.__fieldParent)
    
    # TODO: set angle in radians to top (4.71), down (1.57), left (3.14), right (0)    
    def rotate(self, angle):
        self.angle = angle
    
    # moves the player 
    def move(self, animStoppedCallback):
        if not self.__user.isDead():
            self.running = True
            newPos = (self.x + self.direction[0] * self.size[0], self.y + self.direction[1] * self.size[1])
            time = 80 + 500/self.__user.getSpeedStat()
                        
            self.__animateUser(3, time/4, (self.direction[0] * self.size[0])/4, (self.direction[1] * self.size[1])/4, newPos, animStoppedCallback)
    
    # implements walking animation   
    def __animateUser(self, state, time, xDiff, yDiff, end_pos, callback):
        new_pos = (self.x + xDiff, self.y + yDiff)
        if state == 3:
            self.href = self.__user.getCharImage(3)
        elif state == 2:
            self.href = self.__user.getCharImage(2)
        elif state == 1:
            self.href = self.__user.getCharImage(1)
        else:
            self.href = self.__user.getCharImage(2)
            animObj = avg.LinearAnim(self, "pos", time, self.pos, end_pos)
            animObj.setStopCallback(lambda: callback(self))
            animObj.start()
            return
        
        animObj = avg.LinearAnim(self, "pos", time, self.pos, new_pos)
        if self.__user.isDead():
            self.die()
            return
        else:
            animObj.setStopCallback(lambda: self.__animateUser(state-1, time, xDiff, yDiff, end_pos, callback))
        animObj.start()

    def stopMoving(self, (x, y)):
        if self.direction == (x, y):
            self.direction = (0,0)
    
    # signs user as dead and moves him outside the field  
    def die(self):
        self.__user.die()
        self.href = ""
        self.pos = (0,0)
        
    def isDead(self):
        return self.__user.isDead()
        
    def getPos(self):
        return self.__user.pos
    
    def setPos(self, pos):
        self.__user.pos = pos 
        
    def upgradeBomb(self):
        self.__user.upgradeBomb()
    
    def upgradeSpeed(self):
        self.__user.upgradeSpeed()
        
    def upgradeStrength(self):
        self.__user.upgradeStrength()
        
    def getUserID(self):
        return self.__user.getUserID()
        
    def getUser(self):
        return self.__user

'''
This class is representing the whole node of the playing field.
Attaching it to the main root node will result in drawing the field.
'''
class FieldNode(avg.DivNode):
    destroyableField = 7
    def __init__(self, users, fieldParent, userChangedHandler, gameStateHandler, gameEventHandler, countDownTime=3, seed=None, fieldSize=(15, 15), blockSize=(50, 50), fieldGraphics=FieldGraphics(), **kwargs):
        self.__fieldSize = fieldSize
        self.__blockSize = blockSize
        self.__pixelSize = self.getPixelSize()
        self.__fieldParent = fieldParent
        self.__fieldGraphics = fieldGraphics
        self.__userChanged = userChangedHandler
        self.__gameStateChanged = gameStateHandler
        self.__countDownTime = countDownTime
        self.__gameEventHandler = gameEventHandler
        self.__gameOver = False

        super(FieldNode, self).__init__(id="field", size=self.__pixelSize, **kwargs)
        self.registerInstance(self, self.__fieldParent)

        # TODO: enter Sudden Death if x% of the field is destroyed (#destroyed blocks/#available fields) ????? would have to add callback: remove sudden death timeout
        # TODO: kick bomb ??? really needed? -> animate animated bomb...
        
        # set background image with opacity
        self.__backNode = avg.ImageNode(href=self.__fieldGraphics.back, parent=self, size=self.__pixelSize)
        self.__diffuserNode = avg.RectNode(size=self.__pixelSize,  fillopacity=0.4 , fillcolor="CCCCCC",  color="CCCCCC", parent=self)
        # create random field
        self.__initBlockArray()
        self.__generateRandomField(seed)
        
        # init users
        # always a list, can contain None
        self.__users = []
        for i in range(len(users)):
            if users[i] is not None:
                users[i].setInitPos(self.__fieldSize)
                self.__users.append(UserNode(self, self.__blockSize, users[i]))
        
        # word node for count down before game starts
        self.countDownNode = avg.WordsNode(font="calibri", alignment="center", color="CCCCCC", fontsize=500, pos=(self.size[0]/2, self.size[1]/2-250), text=str(self.__countDownTime), size = self.__pixelSize, parent = self)
       
    # starts count down before game  
    def countDown(self):
        self.__countDown(self.__countDownTime)
        
    def __countDown(self, count):
        if (count > 0):
            self.countDownNode.text = str(count)
            self.countDownNode.opacity = 1
            animObj = avg.LinearAnim(self.countDownNode, "opacity", 1000, 1, 0.6)
            animObj.setStopCallback(lambda: self.__countDown(count-1))
            animObj.start()
        elif count == 0:
            self.countDownNode.text = "Los!"
            self.countDownNode.opacity = 1
            animObj = avg.LinearAnim(self.countDownNode, "opacity", 1000, 1, 0)
            animObj.setStopCallback(lambda: self.__countDown(count-1))
            animObj.start()
        else:
            self.countDownNode.active = False
            self.__gameStateChanged("start")
        
    # starts sudden death
    def startSuddenDeath(self):
        xW = self.__fieldSize[0] - 2
        yW = self.__fieldSize[1] - 2
        suddenDeathOrder = self.__getSuddenDeathOrder(0, 1, xW, yW, xW, '+x', [])
        self.__gameEventHandler("sudden_death")
        self.__suddenDeath(suddenDeathOrder)

    def __suddenDeath(self, order):
        if len(order) > 0 and not self.__isGameOver():
            coord = order.pop(0)
            print coord
            block = self.__blockArray[coord[0]][coord[1]]
            if self.__containsPlayer(coord):
                self.__destroyUser(self.__getPlayerBy(coord))
            block.suddenDeath(lambda: self.__suddenDeath(order))
        else:
            print "Sudden Death finished"
    
    # returns order for sudden death blocks
    def __getSuddenDeathOrder(self, x, y, xW, yW, w, direction, order):
        if w == 0:
            return order
        elif direction == '+x':
            x = x + 1
            for i in range(x, x + w):
                order.append((i,y))
            return self.__getSuddenDeathOrder(i, y, xW, yW-1, yW-1, '+y', order)
        elif direction == '-x':
            x = x - 1
            for i in range(x, x - w, -1):
                order.append((i,y))
            return self.__getSuddenDeathOrder(i, y, xW, yW-1, yW-1, '-y', order)
        elif direction == '+y':
            y = y + 1
            for j in range(y, y + w):
                order.append((x,j))
            return self.__getSuddenDeathOrder(x, j, xW-1, yW, xW-1, '-x', order)
        elif direction == '-y':
            y = y - 1
            for j in range(y, y - w, -1):
                order.append((x,j))
            return self.__getSuddenDeathOrder(x, j, xW-1, yW, xW-1, '+x', order)
        else:
            return None
    
    # computes walking direction by string and rotation angle: 
    # r=right(1,0,0); l=left(-1,0,pi/2), t=top(0,-1,3/2*pi), b=bottom(0,1,pi/2)       
    def compDirByString(self, d):
        x = 0
        y = 0
        angle = 0 # in radians
        # get new coordinate
        if d == "r":
            x = 1
            angle = 0
        elif d == "l":
            x = -1
            angle = 3.14
        elif d == "t":
            y = -1
            angle = 4.71
        elif d == "b":
            y = 1
            angle = 1.57
            
        return (x, y, angle)
 
    # stops walking animation
    def stopPlayer(self, userID, d):
        (x, y, angle) = self.compDirByString(d)
        u = self.__users[userID]
        u.stopMoving((x, y))
    
    # starts walking animation 
    def movePlayer(self, userID, d):
        u = self.__users[userID]
        
        (x, y, angle) = self.compDirByString(d)
        
        # rotate in any case
        u.rotate(angle) 
        # set dir
        u.direction = (x, y)
        # move if possible and not already running
        self.__checkAndMove(x, y, u)  
    
    # check if movement is ok (walkable) then move and check if something should happen (explosion, upgrade)        
    def __checkAndMove(self, x, y, u):
        #check if there's something in between
        u_pos = u.getPos()
        new_pos = (u_pos[0]+x, u_pos[1]+y)
        # isWalkable: block and bomb + containsPlayer: foe player
        blockNode = self.__blockArray[new_pos[0]][new_pos[1]]
        walkable = blockNode.isWalkable()
        selfCollision = self.__containsPlayer(new_pos) #and (self.__getPlayerBy(new_pos) == u.getUserID())
        if not walkable:
            self.__gameEventHandler("crash" , u.getUserID())
        if walkable and not selfCollision:
            if not u.running:
                # catch upgrade
                if blockNode.getUpgrade() == 0:
                    u.upgradeBomb()
                    blockNode.removeUpgrade()
                    self.__userChanged(u.getUser())
                    self.__gameEventHandler("upgrade_collected" , u.getUserID())
                elif blockNode.getUpgrade() == 1:
                    u.upgradeSpeed()
                    blockNode.removeUpgrade()
                    self.__userChanged(u.getUser())
                    self.__gameEventHandler("upgrade_collected" , u.getUserID())
                elif blockNode.getUpgrade() == 2:
                    u.upgradeStrength()
                    blockNode.removeUpgrade()
                    self.__userChanged(u.getUser())
                    self.__gameEventHandler("upgrade_collected" , u.getUserID())

                u.move(self.__animStoppedHandler)
                u.setPos(new_pos)
                
                # check for explosion
                if blockNode.isExploding():
                    self.__destroyUser(u)
            
    # check if a certain field contains a player
    def __containsPlayer(self, pos):
        blocked = False
        for i in range(len(self.__users)):
            blocked = blocked or (self.__users[i].getPos() == pos)
        return blocked
    
    # returns the player of a given position
    def __getPlayerBy(self, pos):
        for i in range(len(self.__users)):
            if self.__users[i].getPos() == pos:
                return self.__users[i]
        return None
    
    # is called when user stops walking        
    def __animStoppedHandler(self, userNode):
        userNode.running = False
        x = userNode.direction[0]
        y = userNode.direction[1]
        if not (userNode.isDead()):
            self.__checkAndMove(x, y, userNode)
            
    def addBomb(self, userID):
        # dependent on bomb attribute
        userNode = self.__users[userID]
        user = userNode.getUser()
        u_pos = userNode.getPos()
        if (user.canPlaceBomb() and not self.__blockArray[u_pos[0]][u_pos[1]].isBombPlaced()): # TODO: test
            self.__blockArray[u_pos[0]][u_pos[1]].addBomb(userNode, self.__detonateBomb)
            self.__gameEventHandler("bomb_placed" , userID)
    
    # detonates a bomb, triggers explosion for all affected fields    
    def __detonateBomb(self, userNode, blockNode):
        if self.__gameOver:
            return
        blockNode.removeBomb()
        user = userNode.getUser()
        user.bombDetonated()
        cur_pos = blockNode.getPos()
        self.__gameEventHandler("bomb_detonated")
        
        # check field in all 4 directions regarding strength
        strength = user.getStrengthStat()
        # detonate here: i == 0
        #print '######################'
        #print 'DETONATION'
        #print 'with strength: ' + str(strength)
        #print '---------------------'
        #print 'self'
        self.__detonateBlock(cur_pos) 

        # detonate right
        #print '---------------------'
        #print 'right'
        for i in range(strength+1):
            if i != 0:
                if self.__detonateBlock((cur_pos[0]+i, cur_pos[1])):
                    break
        #print '---------------------'
        # detonate left
        #print 'left'
        for i in range(strength+1):
            if i != 0:
                if self.__detonateBlock((cur_pos[0]-i, cur_pos[1])):
                    break
        #print '---------------------'
        # detonate bottom
        #print 'bottom'
        for i in range(strength+1):
            if i != 0:
                if self.__detonateBlock((cur_pos[0], cur_pos[1]+i)):
                    break
        #print '---------------------'
        # detonate top
        #print 'top'
        for i in range(strength+1):
            if i != 0:
                if self.__detonateBlock((cur_pos[0], cur_pos[1]-i)):
                    break
                
        #print '######################'
        self.__isGameOver()

    # detonates one block    
    def __detonateBlock(self, pos): 
        blockNode = self.__blockArray[pos[0]][pos[1]]
        res = False
        
        # destroy block
        if blockNode.getBlockType() != -1:
            res = True
            if blockNode.getBlockType() == 0:
                blockNode.removeBlock()
                # drop upgrade by chance 30%
                ran = random.randint(1,10)
                if ran <= 3:
                    #blockNode.setUpgrade(2)
                    blockNode.setUpgrade(random.randint(0,2))
        # check for user, upgrade or bomb to destroy
        else:
            if blockNode.isBombPlaced():
                res = True            
            if self.__containsPlayer(pos):
                self.__destroyUser(self.__getPlayerBy(pos))
                
            blockNode.removeUpgrade()
            blockNode.explode()
        
        # return True if detonation is stopped
        return res
    
    # signs a user as dead and informs root app
    def __destroyUser(self, userNode):
        print "player died: " + str(userNode.getUserID())
        userNode.die()
        self.__userChanged(userNode.getUser())
    
    # returns if game is over   
    def __isGameOver(self):
        if self.__gameOver:
            return True
        # check if game has finished.
        dead = 0
        count = 0
        for i in range(len(self.__users)):
            if self.__users[i] is not None:
                count += 1
                if self.__users[i].isDead():
                    dead += 1
        
        if dead >= (count - 1) :
            self.__gameOver = True
            self.__animateEnd()
            return True
    
    # outro        
    def __animateEnd(self):
        x = self.__fieldSize[0]
        y = self.__fieldSize[1]
        animObj = avg.LinearAnim(self.__backNode, "opacity", 2600, 1, 0)
        animObj.setStopCallback(lambda: self.__gameStateChanged("end"))
        animObj.start()
        for i in range(x):
            for j in range(y):
                #if self.__isUndestroyable(i, j, x, y):
                node = self.__blockArray[i][j]
                animObj = avg.LinearAnim(node, "pos", random.randint(500, 2500), node.pos, (random.randint(-500, self.__pixelSize[0]+500), random.randint(-500, -self.__blockSize[1])))
                animObj.start()
    
    # returns field size in pixels
    def getPixelSize(self):
        return (self.__fieldSize[0]*self.__blockSize[0],self.__fieldSize[1]*self.__blockSize[1])
    
    # creates an empty field
    def __initBlockArray(self):
        self.__blockArray = []
        x = self.__fieldSize[0]
        y = self.__fieldSize[1]
        for i in range(x):
            self.__blockArray.append([])
            for j in range(y):
                self.__blockArray[i].append(self.__addBlockNode((i,j)))
    
    # adds a block to a certain position            
    def __addBlockNode(self, position):
        ref = BlockNode(self, self.__blockSize, position, self.__fieldGraphics)
        return ref   
    
    # generates a random field    
    # seed can be None (system time is used) or int
    def __generateRandomField(self, seed):
        print 'seed: ' + str(seed)
        random.seed(seed)        
        x = self.__fieldSize[0]
        y = self.__fieldSize[1]
        for i in range(x):
            for j in range(y):
                if self.__isUndestroyable(i, j, x, y):
                    self.__blockArray[i][j].setBlockType(1)
                elif not self.__isStartField(i, j, x, y):
                    self.__blockArray[i][j].setBlockType(self.__randomDestroyable())                  
    
    # returns if a block is undestroyable or not             
    def __isUndestroyable(self, i, j, x, y):
        return (not(i % 2 or j % 2) or i == 0 or j == 0 or i == x-1 or j == y-1)
    
    # returns if a block is start area or not 
    def __isStartField(self, i, j, x, y):
        start1 = (i == 1 and j == 1 or i == 1 and j == 2 or i == 2 and j == 1)
        start2 = (i == x-2 and j == 1 or i == x-2 and j == 2 or i == x-3 and j == 1)
        start3 = (i == 1 and j == y-2 or i == 1 and j == y-3 or i == 2 and j == y-2)
        start4 = (i == x-2 and j == y-2 or i == x-2 and j == y-3 or i == x-3 and j == y-2)
        return (start1 or start2 or start3 or start4) 
    
    # 80% chance for destroyable block, else no block
    def __randomDestroyable(self):
        ran = random.randint(1,10)
        if ran <= self.destroyableField:
            return 0
        return -1